slimecing

a fighting game featuring slimes and swords
Log | Files | Refs | README

RebindActionUI.cs (16452B)


      1 using System;
      2 using System.Collections.Generic;
      3 using System.Linq;
      4 using UnityEngine.Events;
      5 using UnityEngine.InputSystem.Utilities;
      6 using UnityEngine.UI;
      7 
      8 ////TODO: localization support
      9 
     10 ////TODO: deal with composites that have parts bound in different control schemes
     11 
     12 namespace UnityEngine.InputSystem.Samples.RebindUI
     13 {
     14     /// <summary>
     15     /// A reusable component with a self-contained UI for rebinding a single action.
     16     /// </summary>
     17     public class RebindActionUI : MonoBehaviour
     18     {
     19         /// <summary>
     20         /// Reference to the action that is to be rebound.
     21         /// </summary>
     22         public InputActionReference actionReference
     23         {
     24             get => m_Action;
     25             set
     26             {
     27                 m_Action = value;
     28                 UpdateActionLabel();
     29                 UpdateBindingDisplay();
     30             }
     31         }
     32 
     33         /// <summary>
     34         /// ID (in string form) of the binding that is to be rebound on the action.
     35         /// </summary>
     36         /// <seealso cref="InputBinding.id"/>
     37         public string bindingId
     38         {
     39             get => m_BindingId;
     40             set
     41             {
     42                 m_BindingId = value;
     43                 UpdateBindingDisplay();
     44             }
     45         }
     46 
     47         public InputBinding.DisplayStringOptions displayStringOptions
     48         {
     49             get => m_DisplayStringOptions;
     50             set
     51             {
     52                 m_DisplayStringOptions = value;
     53                 UpdateBindingDisplay();
     54             }
     55         }
     56 
     57         /// <summary>
     58         /// Text component that receives the name of the action. Optional.
     59         /// </summary>
     60         public Text actionLabel
     61         {
     62             get => m_ActionLabel;
     63             set
     64             {
     65                 m_ActionLabel = value;
     66                 UpdateActionLabel();
     67             }
     68         }
     69 
     70         /// <summary>
     71         /// Text component that receives the display string of the binding. Can be <c>null</c> in which
     72         /// case the component entirely relies on <see cref="updateBindingUIEvent"/>.
     73         /// </summary>
     74         public Text bindingText
     75         {
     76             get => m_BindingText;
     77             set
     78             {
     79                 m_BindingText = value;
     80                 UpdateBindingDisplay();
     81             }
     82         }
     83 
     84         /// <summary>
     85         /// Optional text component that receives a text prompt when waiting for a control to be actuated.
     86         /// </summary>
     87         /// <seealso cref="startRebindEvent"/>
     88         /// <seealso cref="rebindOverlay"/>
     89         public Text rebindPrompt
     90         {
     91             get => m_RebindText;
     92             set => m_RebindText = value;
     93         }
     94 
     95         /// <summary>
     96         /// Optional UI that is activated when an interactive rebind is started and deactivated when the rebind
     97         /// is finished. This is normally used to display an overlay over the current UI while the system is
     98         /// waiting for a control to be actuated.
     99         /// </summary>
    100         /// <remarks>
    101         /// If neither <see cref="rebindPrompt"/> nor <c>rebindOverlay</c> is set, the component will temporarily
    102         /// replaced the <see cref="bindingText"/> (if not <c>null</c>) with <c>"Waiting..."</c>.
    103         /// </remarks>
    104         /// <seealso cref="startRebindEvent"/>
    105         /// <seealso cref="rebindPrompt"/>
    106         public GameObject rebindOverlay
    107         {
    108             get => m_RebindOverlay;
    109             set => m_RebindOverlay = value;
    110         }
    111 
    112         /// <summary>
    113         /// Event that is triggered every time the UI updates to reflect the current binding.
    114         /// This can be used to tie custom visualizations to bindings.
    115         /// </summary>
    116         public UpdateBindingUIEvent updateBindingUIEvent
    117         {
    118             get
    119             {
    120                 if (m_UpdateBindingUIEvent == null)
    121                     m_UpdateBindingUIEvent = new UpdateBindingUIEvent();
    122                 return m_UpdateBindingUIEvent;
    123             }
    124         }
    125 
    126         /// <summary>
    127         /// Event that is triggered when an interactive rebind is started on the action.
    128         /// </summary>
    129         public InteractiveRebindEvent startRebindEvent
    130         {
    131             get
    132             {
    133                 if (m_RebindStartEvent == null)
    134                     m_RebindStartEvent = new InteractiveRebindEvent();
    135                 return m_RebindStartEvent;
    136             }
    137         }
    138 
    139         /// <summary>
    140         /// Event that is triggered when an interactive rebind has been completed or canceled.
    141         /// </summary>
    142         public InteractiveRebindEvent stopRebindEvent
    143         {
    144             get
    145             {
    146                 if (m_RebindStopEvent == null)
    147                     m_RebindStopEvent = new InteractiveRebindEvent();
    148                 return m_RebindStopEvent;
    149             }
    150         }
    151 
    152         /// <summary>
    153         /// When an interactive rebind is in progress, this is the rebind operation controller.
    154         /// Otherwise, it is <c>null</c>.
    155         /// </summary>
    156         public InputActionRebindingExtensions.RebindingOperation ongoingRebind => m_RebindOperation;
    157 
    158         /// <summary>
    159         /// Return the action and binding index for the binding that is targeted by the component
    160         /// according to
    161         /// </summary>
    162         /// <param name="action"></param>
    163         /// <param name="bindingIndex"></param>
    164         /// <returns></returns>
    165         public bool ResolveActionAndBinding(out InputAction action, out int bindingIndex)
    166         {
    167             bindingIndex = -1;
    168 
    169             action = m_Action?.action;
    170             if (action == null)
    171                 return false;
    172 
    173             if (string.IsNullOrEmpty(m_BindingId))
    174                 return false;
    175 
    176             // Look up binding index.
    177             var bindingId = new Guid(m_BindingId);
    178             bindingIndex = action.bindings.IndexOf(x => x.id == bindingId);
    179             if (bindingIndex == -1)
    180             {
    181                 Debug.LogError($"Cannot find binding with ID '{bindingId}' on '{action}'", this);
    182                 return false;
    183             }
    184 
    185             return true;
    186         }
    187 
    188         /// <summary>
    189         /// Trigger a refresh of the currently displayed binding.
    190         /// </summary>
    191         public void UpdateBindingDisplay()
    192         {
    193             var displayString = string.Empty;
    194             var deviceLayoutName = default(string);
    195             var controlPath = default(string);
    196 
    197             // Get display string from action.
    198             var action = m_Action?.action;
    199             if (action != null)
    200             {
    201                 var bindingIndex = action.bindings.IndexOf(x => x.id.ToString() == m_BindingId);
    202                 if (bindingIndex != -1)
    203                     displayString = action.GetBindingDisplayString(bindingIndex, out deviceLayoutName, out controlPath, displayStringOptions);
    204             }
    205 
    206             // Set on label (if any).
    207             if (m_BindingText != null)
    208                 m_BindingText.text = displayString;
    209 
    210             // Give listeners a chance to configure UI in response.
    211             m_UpdateBindingUIEvent?.Invoke(this, displayString, deviceLayoutName, controlPath);
    212         }
    213 
    214         /// <summary>
    215         /// Remove currently applied binding overrides.
    216         /// </summary>
    217         public void ResetToDefault()
    218         {
    219             if (!ResolveActionAndBinding(out var action, out var bindingIndex))
    220                 return;
    221 
    222             if (action.bindings[bindingIndex].isComposite)
    223             {
    224                 // It's a composite. Remove overrides from part bindings.
    225                 for (var i = bindingIndex + 1; i < action.bindings.Count && action.bindings[i].isPartOfComposite; ++i)
    226                     action.RemoveBindingOverride(i);
    227             }
    228             else
    229             {
    230                 action.RemoveBindingOverride(bindingIndex);
    231             }
    232             UpdateBindingDisplay();
    233         }
    234 
    235         /// <summary>
    236         /// Initiate an interactive rebind that lets the player actuate a control to choose a new binding
    237         /// for the action.
    238         /// </summary>
    239         public void StartInteractiveRebind()
    240         {
    241             if (!ResolveActionAndBinding(out var action, out var bindingIndex))
    242                 return;
    243 
    244             // If the binding is a composite, we need to rebind each part in turn.
    245             if (action.bindings[bindingIndex].isComposite)
    246             {
    247                 var firstPartIndex = bindingIndex + 1;
    248                 if (firstPartIndex < action.bindings.Count && action.bindings[firstPartIndex].isPartOfComposite)
    249                     PerformInteractiveRebind(action, firstPartIndex, allCompositeParts: true);
    250             }
    251             else
    252             {
    253                 PerformInteractiveRebind(action, bindingIndex);
    254             }
    255         }
    256 
    257         private void PerformInteractiveRebind(InputAction action, int bindingIndex, bool allCompositeParts = false)
    258         {
    259             m_RebindOperation?.Cancel(); // Will null out m_RebindOperation.
    260 
    261             void CleanUp()
    262             {
    263                 m_RebindOperation?.Dispose();
    264                 m_RebindOperation = null;
    265             }
    266 
    267             // Configure the rebind.
    268             m_RebindOperation = action.PerformInteractiveRebinding(bindingIndex)
    269                 .OnCancel(
    270                     operation =>
    271                     {
    272                         m_RebindStopEvent?.Invoke(this, operation);
    273                         m_RebindOverlay?.SetActive(false);
    274                         UpdateBindingDisplay();
    275                         CleanUp();
    276                     })
    277                 .OnComplete(
    278                     operation =>
    279                     {
    280                         m_RebindOverlay?.SetActive(false);
    281                         m_RebindStopEvent?.Invoke(this, operation);
    282                         UpdateBindingDisplay();
    283                         CleanUp();
    284 
    285                         // If there's more composite parts we should bind, initiate a rebind
    286                         // for the next part.
    287                         if (allCompositeParts)
    288                         {
    289                             var nextBindingIndex = bindingIndex + 1;
    290                             if (nextBindingIndex < action.bindings.Count && action.bindings[nextBindingIndex].isPartOfComposite)
    291                                 PerformInteractiveRebind(action, nextBindingIndex, true);
    292                         }
    293                     });
    294 
    295             // If it's a part binding, show the name of the part in the UI.
    296             var partName = default(string);
    297             if (action.bindings[bindingIndex].isPartOfComposite)
    298                 partName = $"Binding '{action.bindings[bindingIndex].name}'. ";
    299 
    300             // Bring up rebind overlay, if we have one.
    301             m_RebindOverlay?.SetActive(true);
    302             if (m_RebindText != null)
    303             {
    304                 var text = !string.IsNullOrEmpty(m_RebindOperation.expectedControlType)
    305                     ? $"{partName}Waiting for {m_RebindOperation.expectedControlType} input..."
    306                     : $"{partName}Waiting for input...";
    307                 m_RebindText.text = text;
    308             }
    309 
    310             // If we have no rebind overlay and no callback but we have a binding text label,
    311             // temporarily set the binding text label to "<Waiting>".
    312             if (m_RebindOverlay == null && m_RebindText == null && m_RebindStartEvent == null && m_BindingText != null)
    313                 m_BindingText.text = "<Waiting...>";
    314 
    315             // Give listeners a chance to act on the rebind starting.
    316             m_RebindStartEvent?.Invoke(this, m_RebindOperation);
    317 
    318             m_RebindOperation.Start();
    319         }
    320 
    321         protected void OnEnable()
    322         {
    323             if (s_RebindActionUIs == null)
    324                 s_RebindActionUIs = new List<RebindActionUI>();
    325             s_RebindActionUIs.Add(this);
    326             if (s_RebindActionUIs.Count == 1)
    327                 InputSystem.onActionChange += OnActionChange;
    328         }
    329 
    330         protected void OnDisable()
    331         {
    332             m_RebindOperation?.Dispose();
    333             m_RebindOperation = null;
    334 
    335             s_RebindActionUIs.Remove(this);
    336             if (s_RebindActionUIs.Count == 0)
    337             {
    338                 s_RebindActionUIs = null;
    339                 InputSystem.onActionChange -= OnActionChange;
    340             }
    341         }
    342 
    343         // When the action system re-resolves bindings, we want to update our UI in response. While this will
    344         // also trigger from changes we made ourselves, it ensures that we react to changes made elsewhere. If
    345         // the user changes keyboard layout, for example, we will get a BoundControlsChanged notification and
    346         // will update our UI to reflect the current keyboard layout.
    347         private static void OnActionChange(object obj, InputActionChange change)
    348         {
    349             if (change != InputActionChange.BoundControlsChanged)
    350                 return;
    351 
    352             var action = obj as InputAction;
    353             var actionMap = action?.actionMap ?? obj as InputActionMap;
    354             var actionAsset = actionMap?.asset ?? obj as InputActionAsset;
    355 
    356             for (var i = 0; i < s_RebindActionUIs.Count; ++i)
    357             {
    358                 var component = s_RebindActionUIs[i];
    359                 var referencedAction = component.actionReference?.action;
    360                 if (referencedAction == null)
    361                     continue;
    362 
    363                 if (referencedAction == action ||
    364                     referencedAction.actionMap == actionMap ||
    365                     referencedAction.actionMap?.asset == actionAsset)
    366                     component.UpdateBindingDisplay();
    367             }
    368         }
    369 
    370         [Tooltip("Reference to action that is to be rebound from the UI.")]
    371         [SerializeField]
    372         private InputActionReference m_Action;
    373 
    374         [SerializeField]
    375         private string m_BindingId;
    376 
    377         [SerializeField]
    378         private InputBinding.DisplayStringOptions m_DisplayStringOptions;
    379 
    380         [Tooltip("Text label that will receive the name of the action. Optional. Set to None to have the "
    381             + "rebind UI not show a label for the action.")]
    382         [SerializeField]
    383         private Text m_ActionLabel;
    384 
    385         [Tooltip("Text label that will receive the current, formatted binding string.")]
    386         [SerializeField]
    387         private Text m_BindingText;
    388 
    389         [Tooltip("Optional UI that will be shown while a rebind is in progress.")]
    390         [SerializeField]
    391         private GameObject m_RebindOverlay;
    392 
    393         [Tooltip("Optional text label that will be updated with prompt for user input.")]
    394         [SerializeField]
    395         private Text m_RebindText;
    396 
    397         [Tooltip("Event that is triggered when the way the binding is display should be updated. This allows displaying "
    398             + "bindings in custom ways, e.g. using images instead of text.")]
    399         [SerializeField]
    400         private UpdateBindingUIEvent m_UpdateBindingUIEvent;
    401 
    402         [Tooltip("Event that is triggered when an interactive rebind is being initiated. This can be used, for example, "
    403             + "to implement custom UI behavior while a rebind is in progress. It can also be used to further "
    404             + "customize the rebind.")]
    405         [SerializeField]
    406         private InteractiveRebindEvent m_RebindStartEvent;
    407 
    408         [Tooltip("Event that is triggered when an interactive rebind is complete or has been aborted.")]
    409         [SerializeField]
    410         private InteractiveRebindEvent m_RebindStopEvent;
    411 
    412         private InputActionRebindingExtensions.RebindingOperation m_RebindOperation;
    413 
    414         private static List<RebindActionUI> s_RebindActionUIs;
    415 
    416         // We want the label for the action name to update in edit mode, too, so
    417         // we kick that off from here.
    418         #if UNITY_EDITOR
    419         protected void OnValidate()
    420         {
    421             UpdateActionLabel();
    422             UpdateBindingDisplay();
    423         }
    424 
    425         #endif
    426 
    427         private void UpdateActionLabel()
    428         {
    429             if (m_ActionLabel != null)
    430             {
    431                 var action = m_Action?.action;
    432                 m_ActionLabel.text = action != null ? action.name : string.Empty;
    433             }
    434         }
    435 
    436         [Serializable]
    437         public class UpdateBindingUIEvent : UnityEvent<RebindActionUI, string, string, string>
    438         {
    439         }
    440 
    441         [Serializable]
    442         public class InteractiveRebindEvent : UnityEvent<RebindActionUI, InputActionRebindingExtensions.RebindingOperation>
    443         {
    444         }
    445     }
    446 }